// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: fengyang

#include <cmath>
#include <filesystem>
#include <iostream>
#include <memory>

#include <cxxopts.hpp>
#include <fmt/format.h>

#include <boost/asio/high_resolution_timer.hpp>

#include "coin2/app/driver.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/config/args.h"
#include "coin2/exchange/base/executor/sync_executor.h"
#include "coin2/exchange/base/market/market_exchange_api.h"
#include "coin2/feed/fastfeed/writer.h"
#include "coin2/strategy/strategy.h"

// proto
#include "coin2/exchange/feed.pb.h"

using namespace coin2::base::config;
using namespace coin2::app;
using namespace coin2::exchange::base::symbology;
using namespace coin2::exchange::feed;
using coin2::exchange::base::feed::FeedUpdate;
using fastfeed::proto::KlineInterval;
using fastfeed::proto::KlineInterval_Name;
using coin2::exchange::di::ConstantInjectionBuilder;

static int64_t GetDurationOfInterval(KlineInterval interval) {
  static std::map<KlineInterval, int64_t> interval_map = {
    {fastfeed::proto::KLINE_INTERVAL_1HOUR, 60L * 60 * 1000000000},
    {fastfeed::proto::KLINE_INTERVAL_1DAY, 24L * 60 * 60 * 1000000000 }
  };
  auto iter = interval_map.find(interval);
  if (iter == interval_map.end()) {
    NOTREACHED() << "Not support interval " << interval;
    return std::numeric_limits<int64_t>::quiet_NaN();
  }

  return iter->second;
}

class KlinesWriter {
 public:
  explicit KlinesWriter(
      const std::string& out_dir,
      const std::string& date) {
    path_ = fmt::format(
          "{root_dir}/{date}.csv",
          fmt::arg("root_dir", out_dir),
          fmt::arg("date", date));
    if (std::experimental::filesystem::create_directories(path_.parent_path())) {
      LOG(INFO) << "[FileWriter] Created dir: " << path_.parent_path();
    }

    LOG(INFO) << "[FileWriter] Writing to: " << path_;
    of_ = std::ofstream(path_);
    of_.precision(std::numeric_limits<double>::digits10);
  }

  std::ofstream& Writer() {
    return of_;
  }

 private:
  std::experimental::filesystem::path path_;
  std::ofstream of_;
};


class KlineFeedReader : public coin2::strategy::IStrategy {
 public:
  explicit KlineFeedReader(
      const std::string& out_dir,
      const std::string& date)
      : out_dir_(out_dir),
        date_(date) {
  }

  void Init(::coin2::app::Driver* driver) override {
    last_timestamp_ = GetCurrentTimestamp();
    stop_timer_ = std::make_unique<boost::asio::deadline_timer>(
        *(driver->live_executor()->MainIoc()));
    stop_timer_->expires_from_now(boost::posix_time::seconds(5));
    stop_timer_->async_wait(std::bind(&KlineFeedReader::FinishCheck, this));
    // only archive 1 day data
    start_ = TimestampFromString(date_);
    end_ = start_ + ((int64_t)24 * 60 * 60 * 1000000000); 
    klines_writer_ = std::make_unique<KlinesWriter>(out_dir_, date_);
    klines_writer_->Writer() <<
        "mea,symbol,interval,open_timestamp,open,high,low,close,volume,turnover" << std::endl;
  }

  void onCleanup(::coin2::app::Driver* driver) {
    OutputInfo();
  }

  void onKlineFeed(const FeedUpdate& upd) override {
    last_timestamp_ = upd.timestamp();
    auto mea = upd.mea();
    auto symbol = upd.product().relative_norm();
    auto interval = (*upd.klines().Get())->interval;
    auto mea_s_interval = std::make_tuple(mea, symbol, interval);
    auto& klines = upd.klines();
    for (auto iter = klines.RBegin(); iter != klines.REnd(); iter++) {
      if (iter->open_timestamp < start_ ||
          iter->open_timestamp >= end_ ||
          iter->open_timestamp <= kline_last_timestamp_[mea_s_interval]) {
        continue;
      }

      auto& writer = klines_writer_->Writer();
      writer << mea.String() << ","
          << symbol << ","
          << KlineInterval_Name(interval) << ","
          << Iso8601FromTimestamp(iter->open_timestamp) << ","
          << iter->open << ","
          << iter->high << ","
          << iter->low << ","
          << iter->close << ",";

      if (!std::isnan(iter->volume)) writer << iter->volume;
      writer << ",";
      if (!std::isnan(iter->turnover)) writer << iter->turnover;
      writer << std::endl;
      kline_last_timestamp_[mea_s_interval] = iter->open_timestamp;

      // statistics
      int64_t last_open_timestamp = kline_last_timestamp_[mea_s_interval];
      int64_t duration = GetDurationOfInterval(interval);
      if (mea.exchange == coin::proto::Deribit &&
          interval == fastfeed::proto::KLINE_INTERVAL_1DAY) {
        if ((last_open_timestamp - 8 * 60 * 60 * 1000000000LL ) == (end_ - duration)) {
          // fininshed
          kline_done_[mea_s_interval] = true;
        }
      } else if (last_open_timestamp == (end_ - duration)) {
        // fininshed
        kline_done_[mea_s_interval] = true;
      }
    }
  }
 private:
  void FinishCheck() {
    // 2 minutes no data received, exit
    if (GetCurrentTimestamp() - last_timestamp_ > 2 * 60 * 1'000'000'000LL) {
      OutputInfo();
      exit(0);
    }

    stop_timer_->expires_from_now(boost::posix_time::seconds(5));
    stop_timer_->async_wait(std::bind(&KlineFeedReader::FinishCheck, this));
  }
  void OutputInfo() {
    LOG(INFO) << "Total klines: " << kline_last_timestamp_.size() << ", "
        << "finished klines: " << kline_done_.size();
  }
  std::string out_dir_;
  std::string date_;
  int64_t start_;
  int64_t end_;
  std::map<std::tuple<MarketExchangeApi, std::string, KlineInterval>,
           int64_t> kline_last_timestamp_;
  std::map<std::tuple<MarketExchangeApi, std::string, KlineInterval>,
           bool> kline_done_;
  std::unique_ptr<KlinesWriter> klines_writer_;
  std::unique_ptr<boost::asio::deadline_timer> stop_timer_;
  int64_t last_timestamp_ = 0;
};

int main(int argc, char* argv[]) {
  DriverConfig app;
  cxxopts::Options opt("Kline Feed Converter", "Generate Kline");
  AddDriverOptions(&opt);
  opt.add_options()
      ("date", "trading date",
          cxxopts::value<std::string>())
      ("out-dir", "kline data export directory",
          cxxopts::value<std::string>()->default_value(""));

  auto res = opt.parse(argc, argv);
  CHECK(ParseDriverOptions(res, &app)) << "Fail to parse driver";


  KlineFeedReader checker(res["out-dir"].as<std::string>(),
                          res["date"].as<std::string>());

  ConstantInjectionBuilder ci_builder = ConstantInjectionBuilder();
  coin2::app::Driver driver(app, &checker, ci_builder);
  driver.RegisterCleanupHandler();
  driver.Run();

  return 0;
}

